import os
import json
import subprocess

import requests
from optparse import OptionParser
import xml.etree.ElementTree as ET


def sys_command(cmd):
    p = subprocess.Popen(cmd.split(), stdout=subprocess.PIPE)
    out = p.stdout.read()
    return out.decode('utf-8')


def parseXML(xmlfile, node_name):
    # create element tree object
    tree = ET.parse(xmlfile)

    # get root element
    root = tree.getroot()

    # create empty list for news items
    folders = [folder.attrib for folder in root.findall("./folder")]
    devices = [dev.attrib for dev in root.findall("./device")]
    api_key = root.find("./gui/apikey").text
    address = "127.0.0.1:8384" if node_name == "server" else f"127.0.0.1:{8384 + int(node_name[4:])}"
    node_id = None
    for dev in devices:
        if dev["name"] == node_name:
            node_id = dev["id"]
            break

    return {
        "apikey": api_key,
        "address": address,
        "node_id": node_id,
        "node_name": node_name,
        "devices": devices,
        "folders": folders,
    }


def request(url, api_key=None, action='GET', data=None):
    headers = {
        "X-API-Key": api_key
    }
    if action == 'GET':
        response = requests.get(url, headers=headers)
    elif action == 'POST':
        response = requests.post(url, headers=headers, data=json.dumps(data, indent=2))
    elif action == 'PUT':
        response = requests.put(url, headers=headers, data=json.dumps(data, indent=2))
    else:
        response = None
    if not response:
        raise Exception(f"No action {action} found {response} data: {data}")
    if data:
        print(json.dumps(data, indent=2))
    if response.status_code == 200:
        return json.loads(response.content) if action == 'GET' else response.status_code
    else:
        raise Exception(response.status_code)


def generate_device_config(nodes):
    config_list = []
    for node in nodes:
        config = {
            "deviceID": node[-1],
            "name": node[0],
            "addresses": [
                "dynamic"
            ],
            "compression": "metadata",
            "certName": "",
            "introducer": False,
            "skipIntroductionRemovals": False,
            "introducedBy": "",
            "paused": False,
            "allowedNetworks": [],
            "autoAcceptFolders": False,
            "maxSendKbps": 0,
            "maxRecvKbps": 0,
            "ignoredFolders": [],
            "maxRequestKiB": 0,
            "untrusted": False,
            "remoteGUIPort": 0
        }

        config_list.append(config)
    return config_list


def generate_folder(api_key, host="127.0.0.1:8384", label="template"):
    # generate random folder id
    random_info = request(f"http://{host}/rest/svc/random/string?length=10", api_key=api_key)
    folder_config = request(f"http://{host}/rest/config/defaults/folder", api_key=api_key)
    random_str = random_info.get("random", '0123456789').lower()
    folder_config["id"] = f"{random_str[:5]}-{random_str[5:]}"
    folder_config["label"] = label
    folder_config["path"] = f"/var/syncthing/{label}"
    folder_config["devices"] = []  # clear devices
    return folder_config


def remove_folder_device(folder_config, node):
    """
    remove the device from folder
    :param folder_config:
    :param node:
    :return:
    """
    from copy import deepcopy
    folder_config = deepcopy(folder_config)
    for dev in folder_config["devices"]:
        if dev["deviceID"] == node:
            folder_config["devices"].remove(dev)
            return folder_config


def start_container(count):
    """
    start node by docker container
    :return:
    """
    # start syncthing server
    os.system("""docker run --rm -d -p 8384:8384 -v $PWD/sync_server:/var/syncthing \
--hostname=server \
--name=server \
syncthing/syncthing""")

    # start syncthing node
    for i in range(1, count + 1):
        port = 8384 + i
        os.system(f"""docker run --rm -d -p {port}:8384 -v $PWD/sync_node{i}:/var/syncthing \
  --hostname=node{i} \
  --name=node{i} \
  syncthing/syncthing""")

    # check all nodes has running in containers
    output = sys_command("docker container ls")
    if "server" not in output:
        print("error: server not running")
    for n in range(1, count):
        if f"node{n}" not in output:
            print(f"error: node{n} not running")
    return


def stop_container(start, end):
    """
    stop node by docker container
    """
    for i in range(start, end + 1):
        print(sys_command(f"docker stop node{i}"))

    return


def register_node(max_id):
    """
    register syncthing node, get all syncthing nodes info, and register them each other
    :param max_id:
    :return:
    """
    # load config_info
    dict_nodes = dict()
    nodes = ["server"]
    nodes += [f"node{i}" for i in range(1, max_id + 1)]
    ids = []

    # get all nodes config information
    for n in nodes:
        config_path = f"{os.getcwd()}/sync_{n}/config/config.xml"
        if not os.path.exists(config_path):
            print(f"error: config path {config_path} does not exist")
            continue
        dict_nodes.setdefault(n, parseXML(config_path, n))
        ids.append((n, dict_nodes[n].get("node_id", None)))

    # generate folder config information
    folder_config = generate_folder(dict_nodes["server"].get("apikey", None))
    for node in dict_nodes:
        folder_config["devices"].append(
            {
                "deviceID": dict_nodes[node].get("node_id", None),
                "introducedBy": "",
                "encryptionPassword": ""
            })
    # register syncthing node
    for n in nodes:
        host_address = dict_nodes[n].get("address", None)
        api_key = dict_nodes[n].get("apikey", None)
        node_id = dict_nodes[n].get("node_id", None)
        # get node config
        config_info = request(f"http://{host_address}/rest/config", api_key=api_key)
        # add devices to config
        config_info["devices"] = generate_device_config(ids)
        # add folders to config
        # remove myself from folders devices
        config_info["folders"] = [remove_folder_device(folder_config, node_id)]
        # register node put request
        if request(f"http://{host_address}/rest/config", api_key=api_key, data=config_info, action="PUT"):
            print(f"set node {n} configuration successfully, host address {host_address}, api key {api_key}")


if __name__ == '__main__':
    print("""
     ____                   _   _     _             
    / ___| _   _ _ __   ___| |_| |__ (_)_ __   __ _ 
    \___ \| | | | '_ \ / __| __| '_ \| | '_ \ / _` |
     ___) | |_| | | | | (__| |_| | | | | | | | (_| |
    |____/ \__, |_| |_|\___|\__|_| |_|_|_| |_|\__, |
           |___/                              |___/ 
    """)

    parser = OptionParser()
    parser.add_option("--start", action="store_true", dest="start_container")

    parser.add_option("--stop", action="store_true", dest="stop_container")
    parser.add_option("-n", "--number", dest="node_number",
                      help="the node number or ranges begin to end, example: 10 or example 1-10")
    parser.add_option("--registry", action="store_true", dest="register_node",
                      help="register nodes with each other, example: --registry -c 100")

    parser.add_option("-c", "--clients", dest="clients", help="the clients numbers")

    (options, argv) = parser.parse_args()
    if options.start_container:
        if options.clients and options.clients.isdigit():
            print("start_container")
            start_container(int(options.clients))
        else:
            parser.error("the number of clients must be an integer")
            exit("-1")

    elif options.stop_container:
        if options.node_number:
            rnodes = options.node_number.split('-')
            start = rnodes[0]
            end = rnodes[-1]
            if start.isdigit() and end.isdigit() and start <= end:
                print("stop_container")
                stop_container(int(start), int(end))
            else:
                parser.error("the number of nodes must be an integer")
        else:
            parser.error("the number of nodes must be an integer")
            exit("-1")

    elif options.register_node:
        print("register_node")
        if options.clients and options.clients.isdigit() and int(options.clients) > 0:
            register_node(int(options.clients))
        else:
            register_node(1)
